// need the reference "/// <reference path="jquery-x.y.z-vsdoc.js"/>" for jq intellisense to work
// NB: MUST have three '///' not two!

/// <reference path="jquery-1.4.1-vsdoc.js"/>

// globals
var ipaddr,
	usr,
	pwd,
	refreshFreq,
	fastRefreshFreq = "500";  // all freq in ms

// array of objects containing the dps to monitor
// each dp has: 
// - descriptive name
// - path to the dp representing the Read() value
// - path to the dp for Write()ing the new value. if empty it's read only dp
// - class used to link the dp value to its html element(s)
// the class is used to select elements in which to show the value of the point (can be used in multiple locations)
var observedDataPoints_Arr = [
	{ dpName: "DO-1", dpPathRead: "Net/LON/iLON App/Digital Output 1/nvoClaValueFb_1", dpPathWrite: "Net/LON/iLON App/Digital Output 1/nviClaValue_1", dpClass : ".do1value" },
	{ dpName: "DO-2", dpPathRead: "Net/LON/iLON App/Digital Output 2/nvoClaValueFb_2", dpPathWrite: "Net/LON/iLON App/Digital Output 2/nviClaValue_2", dpClass : ".do2value" },
	{ dpName: "DI-1", dpPathRead: "Net/LON/iLON App/Digital Input 1/nvoClsValue_1"   , dpPathWrite: ""  											 , dpClass : ".di1value" },
	{ dpName: "DI-1", dpPathRead: "Net/LON/iLON App/Digital Input 2/nvoClsValue_2"   , dpPathWrite: ""  											 , dpClass : ".di2value" }
];

// alternative representation as a hash with the class as key. Not sure which one I like best.
var observedDataPoints_Obj = {
	".do1value": { dpName: "DO-1", dpPathRead: "Net/LON/iLON App/Digital Output 1/nvoClaValueFb_1", dpPathWrite: "Net/LON/iLON App/Digital Output 1/nviClaValue_1" },
	".do2value": { dpName: "DO-2", dpPathRead: "Net/LON/iLON App/Digital Output 2/nvoClaValueFb_2", dpPathWrite: "Net/LON/iLON App/Digital Output 2/nviClaValue_2" },
	".di1value": { dpName: "DI-1", dpPathRead: "Net/LON/iLON App/Digital Input 1/nvoClsValue_1"   , dpPathWrite: "" },
	".di2value": { dpName: "DI-1", dpPathRead: "Net/LON/iLON App/Digital Input 2/nvoClsValue_2"   , dpPathWrite: "" }
};

	
// MVC model
var dpLocalCache = null;
var controller = null;


/*		
var do1 = "Binder5/LON/ILON151/Digital Output 1/nviClaValue_1",
do2 = "Binder5/LON/ILON151/Digital Output 2/nviClaValue_2",
do1fb = "Binder5/LON/ILON151/Digital Output 1/nvoClaValueFb_1",
do2fb = "Binder5/LON/ILON151/Digital Output 2/nvoClaValueFb_2";
*/


function UpdateIPAddr() {
	ipaddr = $("#ipAddr").val();
}

function RefreshFreqFromSettings() {
	// update ref freq
	return $("#refreshFreq").val();
}

function StopAutoRefresh() {
	$(document).stopTime("RefreshDO");
}

function StartAutoRefresh() {
	// set new timer
	if(dpLocalCache) {
		$(document).everyTime(refreshFreq + "ms", "RefreshDO", dpLocalCache.UpdateObservedDataPoints, 0); // 0 means repeat indefinitely
	}
}

function SetRefreshFreq(freq) {
	StopAutoRefresh();
	// update ref freq
	refreshFreq = freq;  // in ms
	StartAutoRefresh();
}

function WritePreset(dpName, presetValue) {
	var writeBody =	'<Write xmlns="http://wsdl.echelon.com/web_services_ns/ilon100/v4.0/message/">' +
		'<iLonItem>' +
			'<Item>' +
				'<UCPTname>' + dpName + '</UCPTname>' +
				'<UCPTvalue LonFormat="UCPTvalueDef">' + presetValue + '</UCPTvalue>' +
			'</Item>' +
		'</iLonItem>' +
	'</Write>';

	return writeBody;
}

// Assemble a Read() for all specified data points
// dpList: Array (not associative) of strings, each representing a dp full path 
function Read(dpList) {
	
	var itemList = "",
		readBody;
	
	/*
	// get arguments array from implicit variable "arguments"
	var argv = Read.arguments;
	var argc = argv.length;
	*/
	
	// add every dp in the list to the items to read
	for (var i = 0; i < dpList.length; i++) {
		itemList += '<Item xsi:type="Dp_Cfg">' +
			'<UCPTname>' + dpList[i] + '</UCPTname>' +
		'</Item>';
	}
		
	readBody = '<Read xmlns="http://wsdl.echelon.com/web_services_ns/ilon100/v4.0/message/">' +
		'<iLonItem>' + itemList + '</iLonItem>' +
	'</Read>';

	return readBody;
}

// pack up the full envelope for the soap message 
function PrepareFullSoapMessage(body) {
	var soapEnvelope = '<?xml version="1.0" encoding="utf-8"?>' +
	'<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/" ' +
	'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +
	'encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> ' +
		'<Header>' +
			'<messageProperties xmlns="http://wsdl.echelon.com/web_services_ns/ilon100/v4.0/message/">' +
			'</messageProperties>' +
		'</Header>' +
		'<Body>' +
			body +
		'</Body>' +
	'</Envelope>';
	
	return soapEnvelope;
}

// Executes soap call and calls SuccessCallback() if successful
// Returns the full soap message
function MakeSoapCall(body, SuccessCallback) {
	
	var fullSoapMessage = PrepareFullSoapMessage(body);

	//console.log("Ajax call to IP " + ipaddr);
	var xhr = $.ajax({
		//url: "http://" + ipaddr + "/WSDL/iLON100.wsdl",
		url: "/WSDL/iLON100.wsdl",
		username: "ilon",
		password: "ilon",
		type: "POST",
		timeout: 2000, // in milliseconds
		dataType: "xml",
		data: fullSoapMessage,
		contentType: "text/xml; charset=\"utf-8\"",
		success: SuccessCallback,
		error: function(XMLHttpRequest, textStatus, errorThrown) {
			if (textStatus) { console.log("Error status: " + textStatus); }
			if (errorThrown) { console.log("Error thrown: " + errorThrown.message); }
			console.log("Possible cross-domain scripting issues. Correct i.Lon's IP address?");
		},
		complete: function(textStatus, xmlHttpRequest) {
			console.log("  Complete status: " + textStatus);
		}
	});
	return fullSoapMessage;
}

function IlonItemNode2Json(data) {
	
	// convert to json the iLonItem node
	var rootJSON = $.xmlToJSON(data.getElementsByTagName("iLonItem")[0]),
		faults = rootJSON.UCPTfaultCount[0].Text;
	
	// Check errors UCPTfaultCount
	if ( faults !== '0') {
		console.log("UCPTfaultCount says there are " + faults + " errors");
		return; // do nothing
	}
	
	return rootJSON;
}

// Return the preset in UCPTvalue if any, otherwise return the string value 
// UCPTvalue: nodeset, not node
function GetCurrentValue_PresetIfAny(UCPTvalue) {
	// get the preset if there is one (i.e. lonformat is UCPTvalueDef)
	if (UCPTvalue.contains("@LonFormat", "UCPTvalueDef")) {
		var currentPreset = UCPTvalue.getNodesByAttribute("@LonFormat", "UCPTvalueDef")[0].Text;
		return currentPreset;
	} else {
		// if not get the simple text
		return UCPTvalue[0].Text;
	}
}



/*
controller init: insert dps to observe in model and starts up the model
controller: it's him that keeps the mapping dpPath <=> element id, not the DP object!

Read()
model: every n seconds Reads the dp tp observe to keep them updated; if there is a change, notify the controller
controller: if change is notified, update the value in the view

Write()
view: after user action notifies the controller of value change
controller: send the change to the model (with setdp)
model: if the change is real, propagate the change to the ilon with Write

*/

// MVC model for the value of data points
// DpLocalCacheModel object
var DpLocalCacheModel = function() {
	
	// the array that contains the dp objects
	// we'll use it as has table where 
	// key = fullPath
	// value = dp object
	var dpCache = []; // "[]" is equivalent to "new Array()"
	
	// Data point object; must be initialised with the following params (in order):
	// newFullPath: string  
	// newJsonRepresentation: object representing the <Item> node (not nodeset!) according to the x2j.js plugin for jQuery
	// 	Note: if correct then 
	// 		jsonRepresentation === rootJSON.Item[n]
	// 	and
	//		jsonRepresentation.UCPTvalue[0].Text === rootJSON.Item[n].UCPTname[0].Text 
	var DataPoint = function() {
		
		switch( arguments.length ) {
			case 1:
				this.fullPath = arguments[0];
				this.jsonRepresentation = null;
				break;
			case 2:
				this.fullPath = arguments[0];
				this.jsonRepresentation = arguments[1];
				break;
			default: 
				console.log("Missing arguments in DataPoint constructor");
		}
	
		this.CurrentPresetOrValue = function() {
			if (this.jsonRepresentation) {
				return GetCurrentValue_PresetIfAny(this.jsonRepresentation.UCPTvalue);
			} else {
				return null;
			}
		};
	};
	
	// Add or updates a dp in the cache
	this.SaveDataPointInCache = function(dpFullPath, dpJsonRep ) {
		
		// if the dp is already in cache it gets overwritten
		dpCache[dpFullPath] = new DataPoint(dpFullPath, dpJsonRep);
		console.log("SaveDataPointInCache() " + dpFullPath);
		
		// notify the controller here? (too? Only?)
	};
	
	// Setup the list of dp to monitor
	this.ObserveDataPointPath = function(dpFullPath) {
		dpCache[dpFullPath] = new DataPoint(dpFullPath);
	};
	
	this.DataPoint = function(dpFullPath) {
		return dpCache[dpFullPath];
	};
		
	this.UpdateObservedDataPoints = function() {
		// the array of simple dpPaths
		var list = [],
		// the time of last update
		 	lastUpdateTime;
		
		// for each dp under observation, find those that are actually in the visible page; no point updating them otherwise
		$.each(observedDataPoints_Arr, function (index, value) {
			// get the .current (=visible) DIV and--only within that--get the class that represents the current dp
			$("div.current" + " " + value.dpClass).each( function() {
				// and save the dp path
				list.push(value.dpPathRead);
				console.log("Pushed " + value.dpPathRead);
				// do this only once for each class found
				return false;
			});
		});
		// if there are visible points to update, update them
		if (list.length !== 0) {
			
			// set ref freq to the settings value: assuming the user will actually stay on the current page with some visible points
			if (refreshFreq !== RefreshFreqFromSettings()) {
				SetRefreshFreq(RefreshFreqFromSettings());
			}
			
			/*
			// ilon date format: 2010-04-09T16:09:22.320Z      
			var d = new Date();
			var currentDate = sprintf("%d-%02d-%02dT" + "%02d:%02d:%02d.%dZ",
				d.getUTCFullYear() 	   , 
				d.getUTCMonth() +1	   , // months are 0-11
				d.getUTCDate()         , // days are 1-31
				d.getUTCHours() 	   ,
				d.getUTCMinutes() 	   ,
				d.getUTCSeconds() 	   ,
				d.getUTCMilliseconds() 
			);
			
			// read only the points changed after a given time
			var onlyReadChangedPoints_Body = '<?xml version="1.0" encoding="utf-8" ?><Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><Header><messageProperties xmlns="http://wsdl.echelon.com/web_services_ns/ilon100/v4.0/message/"></messageProperties></Header><Body><List xmlns="http://wsdl.echelon.com/web_services_ns/ilon100/v4.0/message/"><iLonItem><xSelect>//Item[UCPTlastUpdate&gt;"' + currentDate + '"]</xSelect></iLonItem></List></Body></Envelope>';
			*/
			
			MakeSoapCall(Read(list), function(data, textStatus, xmlHttpRequest) {
				console.log("Read() success status: " + textStatus);

				// convert to json the iLonItem node
				var rootJSON = IlonItemNode2Json(data);

				// for each dp read
				for (var i = 0; i < rootJSON.Item.length; i++) {

					// what dp are we updating?
					var dpName = rootJSON.Item[i].UCPTname[0].Text;
					var latestValue = GetCurrentValue_PresetIfAny(rootJSON.Item[i].UCPTvalue);

					// print to console name and value of dp
					console.log(dpName + " = " + latestValue);

					// save the new values for the point
					dpLocalCache.SaveDataPointInCache(dpName, rootJSON.Item[i]);
				}
				// tell the controller there are new values to update in the UI
				controller.DpCacheUpdated();
			});
		} else {
			// if there were no visible points to update, set the ref freq to a faster one, so to be faster to update the UI as soon as
			// the user changes page
		    console.log("No visible points to update, fast refreshing");
			if (refreshFreq !== fastRefreshFreq) {
				SetRefreshFreq(fastRefreshFreq);
			}
		}	
	};
};

var Controller = function() {
	// updates the DOM of the dps sing the original points hash as map dp=>DOM element
	this.DpCacheUpdated = function() {
		
		// for every observed point
		$.each(observedDataPoints_Arr, function(index, value) {
			
			var sel = value.dpClass;
			var newValue = dpLocalCache.DataPoint(value.dpPathRead).CurrentPresetOrValue();
			
			// if they have never appeared on a .current DIV, some points might not have ever been read
			// and therefore not exist in the local cache. If that's the case just skip to next
			if (!newValue) {
				return;
			}
			
			console.log("Updating selector \"" + sel + "\" with value \"" + newValue + "\"" );
			
			// set the (new) value in the right DOM elements  
			// if it's a simple element
			
			// bad -- var tagName = document.getElementById(sel).object.nodeName.toLowerCase();
			
			// get the node (=tag) type of the element to update and update accordingly
			// the sel is the id (which is supposed to be unique), so in practice .each() will run only once
			$(sel).each(function() {
				if (this.nodeName.toLowerCase() === "select") {
					// if it's a select box
					//
					// if value is one of the preset, set it
					// if it's something else, unhide the hidden OPTION and set that
					//
					// SetSelectWithValue(selectElement, newValue)
					/*
						TODO 
					*/
					
					// array of option elements' value
					var optionValues = [];
					// array of option elements' text
					var optionTexts = [];

				  	// iterate through all option elements
				  	// NOTE: the second $ parameter scopes the first, thereby limiting 
					// 		 the search for "option" only on the "this" object, i.e. this specific <select>
					$("option", this).each(function() {
					    // get value/text and push it into respective array
					    optionValues.push($(this).val());
					    optionTexts.push($(this).text());
				  	});

					// if the new value is not one of the options then only modify the select text
					for (var i in optionValues) {
						if (optionValues.hasOwnProperty(i)) {
							// if it's one of the options, set that
							if (newValue === optionValues[i]) {
								$(this).val(newValue);
								break; // exit for loop
							} 
						}
					}
					// if the value wasn't set in the for loop
					if ($(this).val() !== newValue ) {
							$("option[value=\"Hidden\"]", this).text(newValue).attr('selected', 'selected');
					}
				} else if (this.nodeName.toLowerCase() === "input") {
					// if it's a checkbox
					$(this).attr('checked', (newValue === "ON" || newValue === "100.0 1")?true:false);
				} else {
					console.log("Cannot set value %s for a node <%s>. Element not supported", newValue, this.nodeName.toLowerCase() );
				}
			});
		});
	};
};


// jQuery documentReady execution
$(function() {

	// Get the party started
	function Init() {
		// update ip address	
		//UpdateIPAddr();
		
		controller = new Controller();
		
		// set up the MVC model object 
		dpLocalCache = new DpLocalCacheModel();
		
		// set up list of DPs to monitor
		$.each(observedDataPoints_Arr, function(index, value) {
			dpLocalCache.ObserveDataPointPath(this.dpPathRead);
		});
			
		// start monitoring
		dpLocalCache.UpdateObservedDataPoints();
		// update refresh freq with freq from settings
		SetRefreshFreq(RefreshFreqFromSettings());
	}
	Init();

	// keep properties updated
	$("#ipAddr").keyup(function() {
		UpdateIPAddr();
	}).keyup();

	$("#refreshFreq").change(function() {
		SetRefreshFreq(RefreshFreqFromSettings());
	}).change();

	

	/////////////////////// Set Write() events for each point class ///////////////////////////

	$.each(observedDataPoints_Arr, function(index, value) {
		
		// this refers to the current element of the array, in this case our dp object
		var dpClass = this.dpClass;
		var dpPath = this.dpPathWrite; 
		
		// if read only dp, just set to ignore the clicks
		if (dpPath === "") {
			$(dpClass).click( function() {
				return false; // inhibit the click
			});
			return;
		}
		// if not, set the events to handle the clicks:

		//  mousedown() comes before change()
		$(dpClass).mousedown(function() {
			// suspend the autorefresh timer
			//console.log("this = " + this);
			//console.log("$.(this) = " + $(this))
			console.log("Suspending autorefresh on mousedown()");
			StopAutoRefresh();
		});
		
		// actually change the value and resume the autorefresh timer
		$(dpClass).change(function() {
			
			var newValue;	
		
			// get the new value from the DOM itself
			if (this.nodeName.toLowerCase() === "input") {
				// if it's a checkbox
				newValue = ($(this).attr('checked')) == true? "ON" : "OFF";
			} else 	{
				// if it's something else (select, slider, etc.)
				newValue = $(this).val();
			}
		
			// if the user clicks on the non-preset read-only value, ignore the selection
			if (newValue === "Hidden") {
				return false;
			}
			
			console.log("New value: " + newValue);
			
			// set new state
			MakeSoapCall(WritePreset(dpPath, newValue), function(data, textStatus, xmlHttpRequest) {
				console.log("Write success status: " + textStatus);
			});
		
			console.log("Resuming autorefresh on change()");
			StartAutoRefresh();
		});
	});
});



